/*
 * Copyright 2010, 2011, 2012 mapsforge.org
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.mapsforge.android.maps.kml;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;

import net.opengis.kml.BoundaryType;
import net.opengis.kml.Coordinates;
import net.opengis.kml.DocumentType;
import net.opengis.kml.FeatureType;
import net.opengis.kml.GeometryType;
import net.opengis.kml.KmlType;
import net.opengis.kml.LineStringType;
import net.opengis.kml.LinearRingType;
import net.opengis.kml.PlacemarkType;
import net.opengis.kml.PointType;
import net.opengis.kml.PolygonType;

import org.jibx.runtime.BindingDirectory;
import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IUnmarshallingContext;
import org.jibx.runtime.JiBXException;
import org.mapsforge.android.maps.overlay.ArrayItemizedOverlay;
import org.mapsforge.android.maps.overlay.ArrayKMLItemizedOverlay;
import org.mapsforge.android.maps.overlay.ArrayWayOverlay;
import org.mapsforge.android.maps.overlay.KMLOverlayItem;
import org.mapsforge.android.maps.overlay.Overlay;
import org.mapsforge.android.maps.overlay.OverlayWay;
import org.mapsforge.core.model.GeoPoint;

import android.graphics.Paint;
import android.graphics.drawable.Drawable;

/**
 * @author art class that represents a JiBX kml binding. version 2.2 and it provides methods to parse kml files and to
 *         obtain Overlays from it.
 */
public class KMLOpenGis22Binding extends KMLBinding {

	private KmlType cached = null;
	private ArrayKMLItemizedOverlay itemizedOverlay = null;

	private static final String BINDING_PACKAGE = "net.opengis.kml";
	private static final String BINDING_NAME = "binding";

	/**
	 * Creates a new KML Binding for schema net.opengis.kml.2.2 with the following Overlay style:
	 * 
	 * @param defaultItemMarker
	 *            the default Item Marker
	 * @param defaultPolyPaintFill
	 *            the Default Polygon Paint Fill
	 * @param defaultPolyPaintOutline
	 *            the Default Polygon Paint Outline
	 * @param defaultLinePaintFill
	 *            the default Line Paint fill
	 * @param defaultLinePaintOutline
	 *            the default line paint outline
	 */
	public KMLOpenGis22Binding(Drawable defaultItemMarker, Paint defaultPolyPaintFill, Paint defaultPolyPaintOutline,
			Paint defaultLinePaintFill, Paint defaultLinePaintOutline) {
		super();
		this.defaultItemMarker = defaultItemMarker;
		this.defaultPolyPaintFill = defaultPolyPaintFill;
		this.defaultPolyPaintOutline = defaultPolyPaintOutline;
		this.defaultLinePaintFill = defaultLinePaintFill;
		this.defaultLinePaintOutline = defaultLinePaintOutline;
		this.itemizedOverlay = new ArrayKMLItemizedOverlay(this.defaultItemMarker);
	}

	@Override
	public boolean isValidKML(File kmlFile) {
		// search a better way to check?
		KmlType out = null;
		boolean res = false;

		try {
			out = checkCache(kmlFile);

			if (out != null) {
				// unmarshal OK... assuming all went smooth
				res = true;
			}
		} catch (FileNotFoundException e) {
			res = false;
			LOGGER.log(Level.FINER, "KMLOpenGis22Binding.isValid returning false due to FIleNotFoundException.");
			LOGGER.log(Level.FINEST, null, e);
		} catch (JiBXException e) {
			res = false;
			LOGGER.log(Level.FINER, "KMLOpenGis22Binding.isValid returning false due to JiBXException.");
			LOGGER.log(Level.FINEST, null, e);
		} catch (java.lang.ClassCastException e) {
			res = false;
			LOGGER.log(Level.FINER, "KMLOpenGis22Binding.isValid returning false due to ClassCastException.");
			LOGGER.log(Level.FINEST, null, e);
		}

		return res;
	}

	private KmlType checkCache(File kmlFile) throws FileNotFoundException, JiBXException {
		FileReader fr;
		KmlType out = null;

		if (this.cachedFile != null && this.cachedFile.equals(kmlFile) && this.cached != null)
			out = this.cached;
		else {
			this.cachedFile = kmlFile;

			fr = new FileReader(kmlFile);

			IBindingFactory jc = BindingDirectory.getFactory(BINDING_NAME, BINDING_PACKAGE);
			IUnmarshallingContext unmarshaller = jc.createUnmarshallingContext();
			out = (KmlType) unmarshaller.unmarshalDocument(fr, BINDING_NAME);

			this.cached = out;
		}

		return out;
	}

	@Override
	public Collection<? extends Overlay> parseKML(File kmlFile, String backtrackURL) throws FileNotFoundException,
			JiBXException {
		List<Overlay> tmpList = Collections.synchronizedList(new ArrayList<Overlay>(INITIAL_CAPACITY));

		// Reading the File
		KmlType out = checkCache(kmlFile);

		if (out == null) {
			JiBXException e = new org.jibx.runtime.JiBXException("Not Recognized Binding Package");
			throw e;
		}

		DocumentType doc = (DocumentType) out.getFeature();// .getFeature();

		// For each feature in the document, determine it's type, and try to assign it to a suitable Overlay
		for (int i = 0; i < doc.getFeatureList().size(); i++) {
			FeatureType fea = doc.getFeatureList().get(i);
			if (fea instanceof net.opengis.kml.PlacemarkType) {
				// We have a placemark, let's get the geometry associated
				PlacemarkType pl = (PlacemarkType) doc.getFeatureList().get(i);
				Overlay o = kmlPlacemarkToOverlay(pl);

				// Add the overlay to our list
				if (o != null) {
					if (o instanceof ArrayKMLItemizedOverlay) {
						((ArrayKMLItemizedOverlay) o).setBacktrackURL(backtrackURL);
					} else
						tmpList.add(o);
				}
			}
		}

		// the points are special... and all gathered into the itemizedOverlay
		// we must add this special overlay here
		tmpList.add(this.itemizedOverlay);

		// reset that?
		this.itemizedOverlay = new ArrayKMLItemizedOverlay(this.defaultItemMarker);

		return tmpList;
	}

	/**
	 * Tries to convert a KML Placemark to an existing Overlay which represents it properly.
	 * 
	 * @param place
	 *            a KML PlacemarkType
	 * @return The most suitable Overlay representation of the Geometry and info of the placemark
	 */
	private Overlay kmlPlacemarkToOverlay(PlacemarkType place) {
		Overlay result = null;

		GeometryType geometry = place.getGeometry();

		// Point Geometry represented using itemizedOverlay
		if (geometry instanceof net.opengis.kml.PointType) {

			result = kmlPointToOverlay(place);

		} else if (geometry instanceof net.opengis.kml.PolygonType) {

			result = kmlPolygonToOverlay(place);

		} else if (geometry instanceof net.opengis.kml.LineStringType) {

			result = kmlLineStringToOverlay(place);

		} else {
			// TODO: treat unimplemented geometries correctly
			LOGGER.log(Level.WARNING, "KML Geometry: " + geometry.getClass().getCanonicalName()
					+ " treatment is not yet implemented. Sorry.");
		}
		return result;
	}

	private Overlay kmlPointToOverlay(PlacemarkType place) {

		GeometryType geometry = place.getGeometry();

		PointType p = (PointType) geometry;

		// create a GeoPoint with the latitude and longitude coordinates
		// TODO: modify the JiBX auto-generated binding to create a more usable coordinates
		List<GeoPoint> cords = kmlCoordinatesToGeoPoints(p.getCoordinates());

		// If all went right there must be only one coordinate in the cords List
		if (cords.size() > 0) {
			GeoPoint geoPoint = cords.get(0);

			// create an KMLOverlayItem with title and description and the placemark id
			KMLOverlayItem item = new KMLOverlayItem(geoPoint, place.getName().toString(), place.getDescription()
					.toString(), place.getId(), this.cachedFile, null);
			// TODO: find a way to save the place.getId(); info.

			// add the OverlayItem to the ArrayItemizedOverlay
			this.itemizedOverlay.addItem(item);
		}

		return this.itemizedOverlay;
	}

	private Overlay kmlPolygonToOverlay(PlacemarkType place) {

		GeometryType geometry = place.getGeometry();

		PolygonType p = (PolygonType) geometry;

		// TODO: treat the inner boundary too?
		List<GeoPoint> cords = kmlCoordinatesToGeoPoints(p.getOuterBoundaryIs().getLinearRing().getCoordinates());

		ArrayWayOverlay wayOverlay = new ArrayWayOverlay(this.defaultPolyPaintFill, this.defaultPolyPaintOutline);

		GeoPoint[] temp = cords.toArray(new GeoPoint[cords.size()]);
		GeoPoint[][] cordList = new GeoPoint[][] { temp };

		OverlayWay overlayWay = new OverlayWay(cordList, this.defaultPolyPaintFill, this.defaultPolyPaintOutline);

		wayOverlay.addWay(overlayWay);

		return wayOverlay;
	}

	private Overlay kmlLineStringToOverlay(PlacemarkType place) {
		GeometryType geometry = place.getGeometry();

		LineStringType l = (LineStringType) geometry;

		List<GeoPoint> cords = kmlCoordinatesToGeoPoints(l.getCoordinates());

		// TODO: add a better logic to fall to the first non null Fill defined by the user?
		Paint fill = this.defaultLinePaintFill;
		Paint outline = this.defaultLinePaintOutline;

		ArrayWayOverlay wayOverlay = new ArrayWayOverlay(fill, outline);

		GeoPoint[] temp = cords.toArray(new GeoPoint[cords.size()]);
		GeoPoint[][] cordList = new GeoPoint[][] { temp };

		OverlayWay overlayWay = new OverlayWay(cordList, fill, outline);

		wayOverlay.addWay(overlayWay);

		return wayOverlay;
	}

	private static List<GeoPoint> kmlCoordinatesToGeoPoints(Coordinates c) {

		List<GeoPoint> cords = new ArrayList<GeoPoint>();

		for (int i = 0; i < c.getStrings().size(); i++) {
			String[] s = c.getStrings().get(i).split(",");
			try {
				double lon = Double.parseDouble(s[0]);
				double lat = 0.0;
				if (s.length > 1 && s[1] != null)
					lat = Double.parseDouble(s[1]);
				else if (s.length > 2 && s[2] != null)
					lat = Double.parseDouble(s[2]);
				else if (s.length == 1 && c.getStrings().size() > i + 1) {
					lat = Double.parseDouble(c.getStrings().get(i + 1));
					LOGGER.log(Level.INFO, "Using this coordinates: lat:" + lat + " lon: " + lon);
					GeoPoint geoPoint = new GeoPoint(lat, lon);
					cords.add(geoPoint);
					break;
				} else
					LOGGER.log(Level.WARNING, "kmlCoordinates not formatted as expected: " + c.getStrings());
				LOGGER.log(Level.INFO, "Using this coordinates: lat:" + lat + " lon: " + lon);
				GeoPoint geoPoint = new GeoPoint(lat, lon);
				cords.add(geoPoint);
			} catch (java.lang.ArrayIndexOutOfBoundsException e) {
				LOGGER.log(Level.WARNING, "kmlCoordinates not formatted as expected: " + c.getStrings(), e);
			}
		}

		return cords;
	}

	@Override
	protected String getBindingName() {
		return BINDING_NAME;
	}

	@Override
	protected String getBindingPackage() {
		return BINDING_PACKAGE;
	}

	@Override
	public String composeKML(Collection<? extends Overlay> features) {
		// TODO Simple version, extend it and reverse all kmlToThings functions
		KmlType k = new KmlType();

		DocumentType doc = new DocumentType();

		List<FeatureType> list = new ArrayList<FeatureType>();

		for (Iterator<? extends Overlay> i = features.iterator(); i.hasNext();) {
			Overlay o = i.next();
			if (o instanceof ArrayKMLItemizedOverlay) {
				for (int idx = 0; idx < ((ArrayKMLItemizedOverlay) o).size(); idx++) {
					// convert all the Overlay Items
					PlacemarkType pl = new PlacemarkType();
					PointType point = new PointType();
					ArrayList<String> s = new ArrayList<String>();
					s.add(((ArrayKMLItemizedOverlay) o).getItem(idx).getPoint().getLongitude() + ","
							+ ((ArrayKMLItemizedOverlay) o).getItem(idx).getPoint().getLatitude());
					Coordinates coo = new Coordinates();
					coo.setStrings(s);
					point.setCoordinates(coo);
					pl.setGeometry(point);
					pl.setName(((ArrayKMLItemizedOverlay) o).getItem(idx).getTitle());
					pl.setDescription("Point Generated by erdapfel");
					list.add(pl);
				}
			} else if (o instanceof ArrayItemizedOverlay) {
				// TODO: how can we access this? modify the ArrayItemizedOverlay class to provide access?
			} else if (o instanceof ArrayWayOverlay) {
				Collection<OverlayWay> wayList = ((ArrayWayOverlay) o).getWays();

				for (Iterator<OverlayWay> owi = wayList.iterator(); owi.hasNext();) {
					OverlayWay guay = owi.next();
					GeoPoint[] nodes = guay.getWayNodes()[0];

					PlacemarkType pl = new PlacemarkType();
					PolygonType poly = new PolygonType();
					BoundaryType outerBoundaryIs = new BoundaryType();
					LinearRingType linearRing = new LinearRingType();

					ArrayList<String> s = new ArrayList<String>();
					for (int idx = 0; idx < nodes.length; idx++) {
						s.add(nodes[idx].getLongitude() + "," + nodes[idx].getLatitude());
					}
					Coordinates coo = new Coordinates();
					coo.setStrings(s);

					linearRing.setCoordinates(coo);
					outerBoundaryIs.setLinearRing(linearRing);
					poly.setOuterBoundaryIs(outerBoundaryIs);
					pl.setGeometry(poly);
					pl.setName("erdapfel");
					pl.setDescription("Polygon Generated by erdapfel");
					list.add(pl);
				}
			}
		}
		doc.setFeatureList(list);

		k.setFeature(doc);

		return this.marshalMessage(k);
	}
}
